﻿// Borrowed with compliments to: Colin Eberhardt
// http://www.scottlogic.co.uk/blog/colin/2009/02/relativesource-binding-in-silverlight/
// Slightly modified by John Evans for use in the LUCAStudios.Silverlight.Controls library

namespace LUCAStudios.Silverlight.Controls
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Media;

    /// <summary>
    /// POCO Class For BindingHelper
    /// </summary>
    public class BindingProperties
    {
        /// <summary>
        /// Name of the source property to bind from
        /// </summary>
        public string SourceProperty { get; set; }
        /// <summary>
        /// Element Name
        /// </summary>
        public string ElementName { get; set; }
        /// <summary>
        /// Name of the target property to bind to
        /// </summary>
        public string TargetProperty { get; set; }
        /// <summary>
        /// Converter to use during the binding operation (optional)
        /// </summary>
        public IValueConverter Converter { get; set; }
        /// <summary>
        /// Converter paramerter used by the converter during the binding operation (optional)
        /// </summary>
        public object ConverterParameter { get; set; }
        /// <summary>
        /// Indicates if binding to self
        /// </summary>
        public bool RelativeSourceSelf { get; set; }
        /// <summary>
        /// The Type of the ancestor being bound to
        /// </summary>
        public string RelativeSourceAncestorType { get; set; }
        /// <summary>
        /// Indicates how many heirachy levels up the ancestor is located in the tree
        /// </summary>
        public int RelativeSourceAncestorLevel { get; set; }
        /// <summary>
        /// Default Constructor
        /// </summary>
        public BindingProperties()
        {
            RelativeSourceAncestorLevel = 1;
        }
    }

    /// <summary>
    /// Provides UIElements with a flexible binding agent
    /// </summary>
    public static class BindingHelper
    {
        /// <summary>
        /// View Model Internal Class
        /// </summary>
        public class ValueObject : INotifyPropertyChanged
        {
            private object _value;

            /// <summary>
            /// Value
            /// </summary>
            public object Value
            {
                get { return _value; }
                set
                {
                    _value = value;
                    OnPropertyChanged("Value");
                }
            }

            /// <summary>
            /// Property Changed Event
            /// </summary>
            public event PropertyChangedEventHandler PropertyChanged;

            /// <summary>
            /// Called when a property is chaned in the class
            /// </summary>
            /// <param name="propertyName"></param>
            protected virtual void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }

        /// <summary>
        /// Returns the binding set to a given Dependency Object
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static BindingProperties GetBinding(DependencyObject obj)
        {
            return (BindingProperties)obj.GetValue(BindingProperty);
        }

        /// <summary>
        /// Sets a binding to a given Dependency Object
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="value"></param>
        public static void SetBinding(DependencyObject obj, BindingProperties value)
        {
            obj.SetValue(BindingProperty, value);
        }
        /// <summary>
        /// Backing Field for the Binding Attached Property
        /// </summary>
        public static readonly DependencyProperty BindingProperty =
            DependencyProperty.RegisterAttached("Binding", typeof(BindingProperties), typeof(BindingHelper),
            new PropertyMetadata(null, OnBinding));


        /// <summary>
        /// Property change event handler for BindingProperty
        /// </summary>
        private static void OnBinding(
            DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            var targetElement = depObj as FrameworkElement;

            if (targetElement == null) throw new LUCAControlException("Unable to cast DependencyObject to Framework Element");

            targetElement.Loaded += TargetElement_Loaded;
        }

        private static void TargetElement_Loaded(object sender, RoutedEventArgs e)
        {
            var targetElement = sender as FrameworkElement;

            if (targetElement == null) throw new LUCAControlException("Unable to cast object to Framework Element");

            // get the value of our attached property
            var bindingProperties = GetBinding(targetElement);

            if (bindingProperties.ElementName != null)
            {
                // perform our 'ElementName' lookup
                var sourceElement = targetElement.FindName(bindingProperties.ElementName) as FrameworkElement;

                // bind them
                CreateRelayBinding(targetElement, sourceElement, bindingProperties);
            }
            else if (bindingProperties.RelativeSourceSelf)
            {
                // bind an element to itself.
                CreateRelayBinding(targetElement, targetElement, bindingProperties);
            }
            else if (!string.IsNullOrEmpty(bindingProperties.RelativeSourceAncestorType))
            {

                // navigate up the tree to find the type
                DependencyObject currentObject = targetElement;

                var currentLevel = 0;
                while (currentLevel < bindingProperties.RelativeSourceAncestorLevel)
                {
                    do
                    {
                        currentObject = VisualTreeHelper.GetParent(currentObject);
                    }
                    while (currentObject.GetType().Name != bindingProperties.RelativeSourceAncestorType);
                    currentLevel++;
                }

                var sourceElement = currentObject as FrameworkElement;

                // bind them
                CreateRelayBinding(targetElement, sourceElement, bindingProperties);
            }
        }

        private const BindingFlags dpFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;

        private struct RelayBindingKey
        {
// ReSharper disable NotAccessedField.Local
            public DependencyProperty dependencyObject;
            public FrameworkElement frameworkElement;
// ReSharper restore NotAccessedField.Local
        }

        /// <summary>
        /// A cache of relay bindings, keyed by RelayBindingKey which specifies a property of a specific 
        /// framework element.
        /// </summary>
        private static readonly Dictionary<RelayBindingKey, ValueObject> relayBindings = new Dictionary<RelayBindingKey, ValueObject>();

        /// <summary>
        /// Creates a relay binding between the two given elements using the properties and converters
        /// detailed in the supplied bindingProperties.
        /// </summary>
        private static void CreateRelayBinding(FrameworkElement targetElement, FrameworkElement sourceElement,
            BindingProperties bindingProperties)
        {

            var sourcePropertyName = bindingProperties.SourceProperty + "Property";
            var targetPropertyName = bindingProperties.TargetProperty + "Property";

            // find the source dependency property
            var sourceFields = sourceElement.GetType().GetFields(dpFlags);
            var sourceDependencyPropertyField = sourceFields.First(i => i.Name == sourcePropertyName);
            var sourceDependencyProperty = sourceDependencyPropertyField.GetValue(null) as DependencyProperty;

            // find the target dependency property
            var targetFields = targetElement.GetType().GetFields(dpFlags);
            var targetDependencyPropertyField = targetFields.First(i => i.Name == targetPropertyName);
            var targetDependencyProperty = targetDependencyPropertyField.GetValue(null) as DependencyProperty;


            ValueObject relayObject;
            var relayObjectBoundToSource = false;

            // create a key that identifies this source binding
            var key = new RelayBindingKey
                          {
                              dependencyObject = sourceDependencyProperty,
                              frameworkElement = sourceElement
                          };

            // do we already have a binding to this property?
            if (relayBindings.ContainsKey(key))
            {
                relayObject = relayBindings[key];
                relayObjectBoundToSource = true;
            }
            else
            {
                // create a relay binding between the two elements
                relayObject = new ValueObject();
            }


            // initialise the relay object with the source dependency property value 
            if (sourceDependencyProperty != null) relayObject.Value = sourceElement.GetValue(sourceDependencyProperty);

            // create the binding for our target element to the relay object, this binding will
            // include the value converter
            var targetToRelay = new Binding
                                    {
                                        Source = relayObject,
                                        Path = new PropertyPath("Value"),
                                        Mode = BindingMode.TwoWay,
                                        Converter = bindingProperties.Converter,
                                        ConverterParameter = bindingProperties.ConverterParameter
                                    };

            // set the binding on our target element
            if (targetDependencyProperty != null) targetElement.SetBinding(targetDependencyProperty, targetToRelay);

            if (relayObjectBoundToSource) return;
            // create the binding for our source element to the relay object
            var sourceToRelay = new Binding
                                    {
                                        Source = relayObject,
                                        Path = new PropertyPath("Value"),
                                        Mode = BindingMode.TwoWay
                                    };

            // set the binding on our source element
            if (sourceDependencyProperty != null) sourceElement.SetBinding(sourceDependencyProperty, sourceToRelay);

            relayBindings.Add(key, relayObject);
        }
    }
}
